#include "Model.h"
#include <gl/glut.h>
#include <assimp/DefaultLogger.h>
#include <assimp/Logger.h>
#include "AssimpBypassLog.h"
#include <sys/stat.h>


/************************************************************************/
/*                        PUBLIC METHODS                                */
/************************************************************************/

Model::Model()
{
    this->scene = NULL;
    this->isLoaded = false;
    this->log = NULL;
    this->modelparts = NULL;
}

Model::Model(Log *log)
{
    this->scene = NULL;
    this->isLoaded = false;
    this->log = log;
    this->modelparts = NULL;	

    Assimp::DefaultLogger::create("",Assimp::Logger::NORMAL);
    //Assimp::DefaultLogger::get()->attachStream(new AssimpBypassLog(this->errLog, Assimp::Logger::DEBUGGING), Assimp::Logger::DEBUGGING);
    Assimp::DefaultLogger::get()->attachStream(new AssimpBypassLog(this->log, Assimp::Logger::INFO), Assimp::Logger::INFO);
    Assimp::DefaultLogger::get()->attachStream(new AssimpBypassLog(this->log, Assimp::Logger::WARN), Assimp::Logger::WARN);
    Assimp::DefaultLogger::get()->attachStream(new AssimpBypassLog(this->log, Assimp::Logger::ERR), Assimp::Logger::ERR);
}

Model::~Model()
{
    if (this->scene)
        aiReleaseImport(this->scene);
}

int Model::GetNbModelparts()
{
    return this->nbModelparts;
}

Modelpart **Model::GetModelparts()
{
    return this->modelparts;
}

const std::string &Model::GetModelFilePath()
{
    return this->modelFilepath;
}

const std::string &Model::GetModelFileExtension()
{
    return this->fileExtension;
}

int Model::Load(const char *filepath, float scalefactor, NxPhysicsSDK *sdk, ModelBoundingVolume::SHAPETYPE shapetype)
{
    if(isLoaded == false)
    {
        if(this->log != NULL)
            this->log->LogDebug(StringBuilder() << "Loading model " << std::string(filepath) << "...");

        std::string s = filepath;
        unsigned int lastSlashPos = s.find_last_of('\\');
        if(lastSlashPos == s.npos) lastSlashPos = s.find_last_of('/');
        this->modelFilepath = s.substr(0, lastSlashPos);
        this->fileExtension = s.substr(s.find_last_of('.'), s.length());
        this->scalefactor = scalefactor;

        //Check if file exists
        struct stat stFileInfo;
        bool fileExists;
        int intStat = stat(filepath, &stFileInfo);
        if(intStat == 0) fileExists = true;
        else			 fileExists = false;

        if(fileExists == false)
        {
            if(this->log != NULL)
                this->log->LogError(StringBuilder() << "File " << filepath << " doesn't exists or isn't accessible!");
            return 1;
        }
        //

        this->scene = aiImportFile(filepath,
            aiProcess_CalcTangentSpace       | 
            aiProcess_Triangulate            |
            aiProcess_JoinIdenticalVertices  |
            aiProcess_SortByPType);
        if (!scene) {
            if(this->log != NULL)
                this->log->LogError("Unable to load imported scene!");
            return 1;
        }

        GenerateDisplayLists(sdk, shapetype);

        this->isLoaded = true;
        return 0;
    }
    else
    {
        return 1;
    }	
}

int Model::LoadModelOnly(const char *filepath, float scalefactor)
{
    if(isLoaded == false)
    {
        if(this->log != NULL)
            this->log->LogDebug(StringBuilder() << "Loading model " << filepath << "...");

        std::string s = filepath;
        this->modelFilepath = s.substr(0, s.find_last_of('\\'));
        this->fileExtension = s.substr(s.find_last_of('.'), s.length());
        this->scalefactor = scalefactor;

        //Check if file exists
        struct stat stFileInfo;
        bool fileExists;
        int intStat = stat(filepath, &stFileInfo);
        if(intStat == 0) fileExists = true;
        else			 fileExists = false;

        if(fileExists == false)
        {
            if(this->log != NULL)
                this->log->LogError(StringBuilder() << "File " << filepath << " doesn't exists or isn't accessible!");
            return 1;
        }
        //

        this->scene = aiImportFile(filepath,
            aiProcess_CalcTangentSpace       | 
            aiProcess_Triangulate            |
            aiProcess_JoinIdenticalVertices  |
            aiProcess_SortByPType);
        if (!scene) {
            if(this->log != NULL)
                this->log->LogError("Unable to load imported scene!");
            return 1;
        }

        GenerateDisplayLists(NULL, ModelBoundingVolume::BOX_SHAPE); //shapetype isnt used

        this->isLoaded = true;
        return 0;
    }
    else
    {
        return 1;
    }
}

void Model::Draw()
{
    glCallList(this->completeModelDisplayList);
}

/************************************************************************/
/*                        PRIVATE METHODS                               */
/************************************************************************/

void Model::Color4ToFloat4(const aiColor4D *c, float f[4])
{
    f[0] = c->r;
    f[1] = c->g;
    f[2] = c->b;
    f[3] = c->a;
}

void Model::SetFloat4(float f[4], float a, float b, float c, float d)
{
    f[0] = a;
    f[1] = b;
    f[2] = c;
    f[3] = d;
}

void Model::Color4f(const aiColor4D *color)
{
    glColor4f(color->r, color->g, color->b, color->a);
}

void Model::ApplyMaterial(const aiMaterial *mtl)
{
    float c[4];

    GLenum fill_mode;
    unsigned int ret1, ret2;
    struct aiColor4D diffuse;
    struct aiColor4D specular;
    struct aiColor4D ambient;
    struct aiColor4D emission;
    float shininess, strength;
    int two_sided;
    int wireframe;
    unsigned int max;

    SetFloat4(c, 0.8f, 0.8f, 0.8f, 1.0f);
    if(AI_SUCCESS == aiGetMaterialColor(mtl, AI_MATKEY_COLOR_DIFFUSE, &diffuse))
        Color4ToFloat4(&diffuse, c);
    glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, c);

    SetFloat4(c, 0.2f, 0.2f, 0.2f, 1.0f);
    if(AI_SUCCESS == aiGetMaterialColor(mtl, AI_MATKEY_COLOR_AMBIENT, &ambient))
        Color4ToFloat4(&ambient, c);
    glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, c);

    SetFloat4(c, 0.0f, 0.0f, 0.0f, 1.0f);
    if(AI_SUCCESS == aiGetMaterialColor(mtl, AI_MATKEY_COLOR_EMISSIVE, &emission))
        Color4ToFloat4(&emission, c);
    glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, c);

    max = 1;
    ret1 = aiGetMaterialFloatArray(mtl, AI_MATKEY_SHININESS, &shininess, &max);
    max = 1;
    ret2 = aiGetMaterialFloatArray(mtl, AI_MATKEY_SHININESS_STRENGTH, &strength, &max);
    if((ret1 == AI_SUCCESS) && (ret2 == AI_SUCCESS))
    {
        glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, shininess * strength);

        SetFloat4(c, 0.0f, 0.0f, 0.0f, 1.0f);
        if(AI_SUCCESS == aiGetMaterialColor(mtl, AI_MATKEY_COLOR_SPECULAR, &specular))
            Color4ToFloat4(&specular, c);
        glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, c);
    }
    else 
    {
        glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 0.0f);
        SetFloat4(c, 0.0f, 0.0f, 0.0f, 0.0f);
        glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, c);
    }

    max = 1;
    if(AI_SUCCESS == aiGetMaterialIntegerArray(mtl, AI_MATKEY_ENABLE_WIREFRAME, &wireframe, &max))
        fill_mode = wireframe ? GL_LINE : GL_FILL;
    else
        fill_mode = GL_FILL;
    glPolygonMode(GL_FRONT_AND_BACK, fill_mode);

    max = 1;
    if((AI_SUCCESS == aiGetMaterialIntegerArray(mtl, AI_MATKEY_TWOSIDED, &two_sided, &max)) && two_sided)
        glEnable(GL_CULL_FACE);
    else 
        glDisable(GL_CULL_FACE);
}

void Model::TraverseNodes(const aiScene *scene, const aiNode* node, Modelpart **modelpartsPointerArray, int &arrayIndex, unsigned int &displayListIndex, NxPhysicsSDK *sdk, ModelBoundingVolume::SHAPETYPE shapetype, aiMatrix4x4 parentTransformation)
{
    aiMatrix4x4 completeNodeTransf;

    if(node == scene->mRootNode)
    {
        completeNodeTransf = node->mTransformation;
        parentTransformation = completeNodeTransf;
    }
    else
    {
        completeNodeTransf = node->mTransformation;
        aiMultiplyMatrix4(&completeNodeTransf,&parentTransformation);
        if(node->mNumChildren > 0)
            aiMultiplyMatrix4(&parentTransformation,&(node->mTransformation));
    }


    for (unsigned int n = 0; n < node->mNumMeshes; ++n) {
        const aiMesh* mesh = scene->mMeshes[node->mMeshes[n]];

        //create modelpart
        if(this->log != NULL)
            modelpartsPointerArray[arrayIndex] = new Modelpart(log);
        else
            modelpartsPointerArray[arrayIndex] = new Modelpart();

        //set name for modelpart
        if(node->mName.length > 0) //valid name
        {
            modelpartsPointerArray[arrayIndex]->SetName(node->mName.data);
        }
        else
        {
            modelpartsPointerArray[arrayIndex]->SetName(StringBuilder() << arrayIndex);
        }

        //create displaylist for modelpart
        ModelAiParamInfo maipi(scene, node, mesh, this->fileExtension);
        modelpartsPointerArray[arrayIndex]->SetDisplayListID(CreateDisplayListForMesh(maipi, displayListIndex));
        aiMatrix4x4 modelTMatrix = node->mTransformation;
        aiTransposeMatrix4(&modelTMatrix);
        modelpartsPointerArray[arrayIndex]->SetModelTransformationMatrix(modelTMatrix);
        
        //CREATE BOUNDING VOLUME (if needed)
        if(sdk != NULL) //with BV
        {
            ModelBoundingVolume *bv = NULL;
            if(this->log != NULL)
                bv = new ModelBoundingVolume(sdk, log);
            else
                bv = new ModelBoundingVolume(sdk);
            bv->CalcBoundingVolume(shapetype, maipi, completeNodeTransf, this->scalefactor);
            modelpartsPointerArray[arrayIndex]->SetBoundingVolume(bv);
        }

        arrayIndex++;
    }
        
    //traverse through all children-nodes
    for (unsigned int n = 0; n < node->mNumChildren; ++n) {
        TraverseNodes(scene, node->mChildren[n], modelpartsPointerArray, arrayIndex, displayListIndex, sdk, shapetype, parentTransformation);
    }

}

unsigned int Model::CreateDisplayListForMesh(const ModelAiParamInfo &modelAiInfo, unsigned int &displayListIndex)
{
    const aiMesh *mesh = modelAiInfo.mesh;

    glNewList(displayListIndex, GL_COMPILE); //START LIST

    glPushMatrix();

    glScalef(this->scalefactor, this->scalefactor, this->scalefactor);

    if(mesh->HasNormals() == false) {
        glDisable(GL_LIGHTING);
    } else {
        glEnable(GL_LIGHTING);
    }

    if(mesh->HasVertexColors(0) == true) {
        glEnable(GL_COLOR_MATERIAL);
    } else {
        glDisable(GL_COLOR_MATERIAL);
    }

    //material
    aiMaterial *mat = scene->mMaterials[mesh->mMaterialIndex];
    ApplyMaterial(mat);		

    //texture
    if(mat->GetTextureCount(aiTextureType_NONE) == 0)
    {
        aiTextureType textureTypes[] = {aiTextureType_DIFFUSE, aiTextureType_SPECULAR, aiTextureType_AMBIENT, aiTextureType_EMISSIVE, aiTextureType_HEIGHT, aiTextureType_NORMALS
            ,aiTextureType_SHININESS, aiTextureType_OPACITY, aiTextureType_DISPLACEMENT, aiTextureType_LIGHTMAP, aiTextureType_REFLECTION, aiTextureType_UNKNOWN};
        int numTextureTypes = 12;

        for(int texTypeIndex = 0; texTypeIndex < numTextureTypes; ++texTypeIndex)
        {
            int texCount = mat->GetTextureCount(textureTypes[texTypeIndex]);
            for(int texIndex = 0; texIndex < texCount; ++texIndex)
            {

                aiString texPath;
                aiTextureMapping *texMapping = NULL;
                unsigned int *uvIndex = 0;
                float *blend = NULL;
                aiTextureOp *texOp = NULL;
                aiTextureMapMode *texMapMode = NULL;

                if(mat->GetTexture(textureTypes[texTypeIndex], texIndex, &texPath, texMapping, uvIndex, blend, texOp, texMapMode) == aiReturn_SUCCESS)
                {
                    std::string fullTexPath = this->modelFilepath;
                    fullTexPath += "\\";
                    fullTexPath += texPath.data;

                    if(textures.count(fullTexPath) == 0)
                    {
                        textures[fullTexPath] = new DevilTexture();
                    }	
                    if(textures[fullTexPath]->IsLoaded() == false)
                    {
                        textures[fullTexPath]->Load(fullTexPath.c_str());
                    }

                    textures[fullTexPath]->Use();
                }
                else
                {
                    if(this->log != NULL)
                        this->log->LogError("Texture cant be loaded.");
                }
            }
        }
    }	

    //draw polygons
    for (unsigned int t = 0; t < mesh->mNumFaces; ++t) {
        const aiFace* face = &mesh->mFaces[t];
        GLenum face_mode;

        switch(face->mNumIndices) {
            case 1: face_mode = GL_POINTS; break;
            case 2: face_mode = GL_LINES; break;
            case 3: face_mode = GL_TRIANGLES; break;
            default: face_mode = GL_POLYGON; break;
        }

        glBegin(face_mode);
        for(unsigned int i = 0; i < face->mNumIndices; i++) {
            int index = face->mIndices[i];
            if(mesh->HasVertexColors(0) == true)
                Color4f(&mesh->mColors[0][index]);
            if(mesh->HasNormals() == true) 
                glNormal3fv(&mesh->mNormals[index].x);
            if(mesh->HasTextureCoords(0) == true)
            {
                aiVector3D texCoord = mesh->mTextureCoords[0][index];
                glTexCoord2f(texCoord.x , 1-texCoord.y);
            }
            glVertex3fv(&mesh->mVertices[index].x);
        }
        glEnd();
    }

    glDisable(GL_TEXTURE_2D);

    glPopMatrix();

    glEndList();			 //END LIST

    return displayListIndex++;
}

void Model::GenerateDisplayLists(NxPhysicsSDK *sdk, ModelBoundingVolume::SHAPETYPE shapetype)
{
    //prepare variables
    this->nbModelparts = this->scene->mNumMeshes;
    this->modelparts = new Modelpart*[nbModelparts];
    this->completeModelDisplayList = glGenLists(this->nbModelparts + 1);
    unsigned int displayListIndex = this->completeModelDisplayList + 1;
    int arrayIndex = 0;

    aiMatrix4x4 transformationMatrix;
    //traverse through nodes and create displaylists for each mesh and boundingvolumes
    TraverseNodes(this->scene, this->scene->mRootNode, this->modelparts, arrayIndex, displayListIndex, sdk, shapetype, transformationMatrix);

    glNewList(completeModelDisplayList, GL_COMPILE);	//START DISPLAYLIST

    //check environment opengl state
    GLboolean hadLight = 0;
    GLboolean hadColorMaterial = 0;
    glGetBooleanv(GL_LIGHTING, &hadLight);
    glGetBooleanv(GL_COLOR_MATERIAL, &hadColorMaterial);

    glPushMatrix();

    for(int i = 0; i < this->nbModelparts; ++i)
    {
        glPushMatrix();
        glMultMatrixf((float*)&(this->modelparts[i]->GetModelTransformationMatrix()));
        this->modelparts[i]->Draw();
        glPopMatrix();
    }
    
    glPopMatrix();

    //reset environment opengl state
    if(hadLight)
    {
        glEnable(GL_LIGHTING);
    }
    if(hadColorMaterial)
    {
        glEnable(GL_COLOR_MATERIAL);
    }

    glEndList();										//END DISPLAYLIST
}
